iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
Modern Web

react 學習記錄系列 第 15

[Day15]我的 react 學習記錄 - useReducer

  • 分享至 

  • xImage
  •  

這篇文章的主要內容

簡單介紹useReducer。


useReducer

當 state 較為複雜時可以透過 useReducer 來把更新狀態的邏輯一個個拆開,方便管理。


Syntax

const [state, dispatch] = useReducer(reducer, initialArg, init?)

reducer: reducer 是一個 function,這個 function 可以接收到兩個東西一個是當下最新的 state 跟透過 dispatch function 傳進去的參數。
initialArg: 初始資料,如果沒有 init 這個參數的話會被直接放到 state。
init?: 這是一個 function 可以接到 initialArg 如果初始資料需要經過計算或是處理的話就會使用到這個 function,initialArg 會被放到 init function 裡,經過處理後才會變成 state。

state: 當前最新的 state,跟 useState 的回傳值相同。
dispatch: 用來啟用 reducer 來修改 state 並觸發 re-render。通常 dispatch 會接收一個物件,通常這個物件會有一個 type 屬性,用來定義這一次的 dispatch 觸發什麼事件。

注意事項

  • useReducer hook 只能在元件裡使用,且只能在元件的最外層使用,不能放在迴圈或是判斷式裡,如果有需要可以建立一個新的子元件,放在子元件裡。

  • 嚴格模式開啟時,在開發模式下,react 會在畫面第一次 render 時下快速的進行 mount -> unmount -> mount 的動作確保沒有多餘的 side effect 而發生錯誤,這個動作不應該對發生任何預期外的錯誤。

簡單的看過 useReducer 裡面的每一個參數跟 return 的 value 之後就來看一下範例吧。


計數器 - 單純狀態

來簡單的做一個計數器。

type

// reducer 有哪一些 action
const enum ActionType {
  INCREASE = "INCREASE",
  DECREASE = "DECREASE",
}

// dispatch 接收哪些參數
type CountAction = {
  type: ActionType;
  payload?: number;
};

// state
type CountObj = {
  value: number;
};

reducer

function countReducer(state: CountObj, action: CountAction): CountObj {
  switch (action.type) {
    case ActionType.INCREASE: {
      return { value: state.value + 1 };
    }
    case ActionType.DECREASE: {
      return { value: state.value - 1 };
    }
    default:
      return state;
  }
}

這邊我定義了一個 countReducer,上面有提到 reducer 會接收兩個參數 state 是當前最新的 value,action 則是透過 dispatch 傳進來的參數,這個參數在慣例上會有 type 屬性,用來判斷要做什麼動作。

為了方便閱讀慣例上也會使用 switch 判斷式來對 type 做判斷,像上面這樣,每一個 action 應該回傳一個新的物件來改變 state,跟 useState 一樣 react 會使用 Object.is() 來做比較,所以不要使用 mutable 的方式修改 state,這樣不會觸發 re-render。

完整的 code 如下。

import { useReducer } from "react";

// reducer 有哪一些 action
const enum ActionType {
  INCREASE = "INCREASE",
  DECREASE = "DECREASE",
}

// dispatch 接收哪些參數
type CountAction = {
  type: ActionType;
  payload?: number;
};

// state
type CountObj = {
  value: number;
};

const initNumber: CountObj = {
  value: 0,
};

// reducer
function countReducer(state: CountObj, action: CountAction): CountObj {
  switch (action.type) {
    case ActionType.INCREASE: {
      return { value: state.value + 1 };
    }
    case ActionType.DECREASE: {
      return { value: state.value - 1 };
    }
    default:
      return state;
  }
}

function App() {
  const [count, dispatch] = useReducer(countReducer, initNumber);

  function handleIncrease() {
    dispatch({ type: ActionType.INCREASE });
  }

  function handleDecrease() {
    dispatch({ type: ActionType.DECREASE });
  }

  return (
    <div>
      <h1>Count:{count.value}</h1>
      <button onClick={handleIncrease}>increase</button>
      <button onClick={handleDecrease}>decrease</button>
    </div>
  );
}

reducer

或許會覺得這樣的功能用 useState 來實現可能還比較快,比較容易閱讀,但是如果有一個比較複雜的狀態出現時就不同了。


咖啡廳 - 複雜狀態

假設我開了一間咖啡廳,營業中會有下面這些情形。

  • 賣咖啡: 咖啡杯數減少,營業資金增加。
  • 煮咖啡: 咖啡豆子減少,咖啡杯數增加。
  • 補豆子: 營業金額減少,咖啡豆子增加。

type

會有下面這些行為。

const enum ActionType {
  SELL_COFFEE = "SELL_COFFEE", // 賣咖啡
  SELL_COFFEE_BY_NUM = "SELL_COFFEE_BY_NUM", // 賣 N 杯咖啡
  MAKE_COFFEE = "MAKE_COFFEE", // 煮咖啡
  MAKE_COFFEE_BY_NUM = "MAKE_COFFEE_BY_NUM", // 煮 N 杯咖啡
  REPLENISHMENT = "REPLENISHMENT", // 補豆子
}

reducer

reducer 這裡包含了上面所有的邏輯。

const initialState = {
  coffeeBeans: 10, // 咖啡豆 10 包
  coffee: 3, // 咖啡 3 杯
  revenue: 1000, // 營業資金 1000 元
};

const reducer = (state: State, action: ReducerAction) => {
  switch (action.type) {
    case ActionType.SELL_COFFEE: // 賣咖啡
      return {
        ...state,
        coffee: state.coffee - 1, // 賣掉 1 杯咖啡
        revenue: state.revenue + 80, // 1 杯 80 元
      };
    case ActionType.SELL_COFFEE_BY_NUM: // 賣 N 杯咖啡
      return {
        ...state,
        coffee: state.coffee - (action.num || 0), // 減掉賣出數量
        revenue: state.revenue + (action.num || 0) * 80, // 賣出數量 * 80 元
      };
    case ActionType.MAKE_COFFEE: // 煮咖啡
      return {
        ...state,
        coffeeBeans: state.coffeeBeans - 2, // 消耗 2 包豆子
        coffee: state.coffee + 1, // 增加 1 杯咖啡
      };
    case ActionType.MAKE_COFFEE_BY_NUM: // 煮 N 杯咖啡
      return {
        ...state,
        coffeeBeans: state.coffeeBeans - 2 * (action.num || 0), // 消耗 N * num 包豆子
        coffee: state.coffee + 1 * (action.num || 0), // 增加 N 杯咖啡
      };
    case ActionType.REPLENISHMENT: // 補充咖啡豆
      return {
        ...state,
        coffeeBeans: state.coffeeBeans + 10, // 進貨 10 包咖啡豆
        revenue: state.revenue - 200, // 花費 200 元
      };
    default:
      return state;
  }
};

Component

而元件本身長這樣。

function App() {
  const [number, setNumber] = useState<number>(1);
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleCoffeeNumber({ target }: ChangeEvent<HTMLInputElement>) {
    setNumber(Number(target.value));
  }
  function handleSellCoffee() {
		// 當賣咖啡的時候已經沒有咖啡了的話,就先煮 5 杯咖啡再賣。
    if (state.coffee === 0) {
      dispatch({ type: ActionType.MAKE_COFFEE_BY_NUM, num: 5 });
    }
    dispatch({ type: ActionType.SELL_COFFEE });
  }
  function handleSellCoffeeByNum() {
    dispatch({ type: ActionType.SELL_COFFEE_BY_NUM, num: number });
  }
  function handleMakeCoffee() {
    dispatch({ type: ActionType.MAKE_COFFEE });
  }
  function handleReplenishment() {
    dispatch({ type: ActionType.REPLENISHMENT });
  }

  return (
    <div>
      <h1>useReducer</h1>
      <div>
        <p>咖啡豆: {state.coffeeBeans} 包</p>
        <p>咖啡: {state.coffee} 杯</p>
        <p>營業額: {state.revenue} 元</p>
        <label>
          預定數量:
          <input value={number} onChange={handleCoffeeNumber} />
        </label>
      </div>
      <div>
        <button onClick={handleSellCoffee}>賣咖啡</button>
        <button onClick={handleSellCoffeeByNum}>賣 N 杯</button>
        <button onClick={handleMakeCoffee}>煮咖啡</button>
        <button onClick={handleReplenishment}>補咖啡豆</button>
      </div>
    </div>
  );
}

如果要把上面 reducer 的內容全部放到元件裡面的話,元件裡面會變的非常冗長,不好閱讀跟維護。

useReducer-2

useReducer 讓我們可以把更新狀態的邏輯跟 function handle 拆開,透過 dispatch 的方式來觸發狀態的更新,讓狀態的維護跟修改可以更清楚。


useReducer - react document
下一篇會簡單介紹 useLayoutEffect。

如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium


上一篇
[Day14]我的 react 學習記錄 - createContext & useContext
下一篇
[Day16]我的 react 學習記錄 - useLayoutEffect
系列文
react 學習記錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言